package org.jpropelleralt.ledger.list.impl;

import java.util.IdentityHashMap;
import java.util.Map;

import org.jpropelleralt.box.Box;
import org.jpropelleralt.box.BoxFeatures;
import org.jpropelleralt.box.impl.BoxFeaturesDefault;
import org.jpropelleralt.change.Change;
import org.jpropelleralt.collection.CollectionDeltaType;
import org.jpropelleralt.ledger.Ledger;
import org.jpropelleralt.ledger.impl.LedgerChangeDefault;
import org.jpropelleralt.ledger.list.RecordTransformer;
import org.jpropelleralt.list.ListBox;
import org.jpropelleralt.list.ListChange;
import org.jpropelleralt.list.ListDelta;
import org.jpropelleralt.list.impl.ListDeltaDefault;
import org.jpropelleralt.reaction.Reaction;
import org.jpropelleralt.ref.Ref;
import org.jpropelleralt.universe.Universe;

/**
 * A {@link Ledger} based on transforming elements of a {@link ListBox}
 * into records using a {@link RecordTransformer}
 * 
 * @param <T>		The type of element in the {@link ListBox}
 */
public class ListLedger<T> implements Ledger {

	private final Ref<? extends ListBox<? extends T>> model;
	private final RecordTransformer<T> transformer; 
	
	private final TransformerReaction tReaction;
	private final ListReaction lReaction;
	private final BoxFeaturesDefault features;
	
	private ListBox<? extends T> listBox = null;
	

	private ListLedger(Universe universe, Ref<? extends ListBox<? extends T>> model,
			RecordTransformer<T> transformer) {
		super();
		this.model = model;
		this.transformer = transformer;
		
		//We need to react to changes to the model and transformer
		tReaction = new TransformerReaction();
		lReaction = new ListReaction();
		model.features().addReaction(lReaction);
		transformer.features().addReaction(tReaction);
		
		this.features = new BoxFeaturesDefault(this, universe);

	}

	/**
	 * Create a {@link ListLedger}
	 * @param <T>			The type of list element. 
	 * @param universe		The {@link Universe} this {@link ListLedger} will be part of
	 * @param model			The model, a {@link Ref} to the {@link ListBox} 
	 * 						whose contents we are based on
	 * @param transformer	The {@link RecordTransformer} used to 
	 * 						transform from elements of the {@link ListBox} 
	 * 						to records of this {@link Ledger}
	 * @return				A new {@link ListLedger}
	 */
	public static <T> ListLedger<T> create(Universe universe, Ref<? extends ListBox<? extends T>> model,
			RecordTransformer<T> transformer) {
		return new ListLedger<T>(universe, model, transformer);
	}
	
	/**
	 * Create a {@link ListLedger}
	 * @param <T>			The type of list element. 
	 * @param model			The model, a {@link Ref} to the {@link ListBox} 
	 * 						whose contents we are based on
	 * @param transformer	The {@link RecordTransformer} used to 
	 * 						transform from elements of the {@link ListBox} 
	 * 						to records of this {@link Ledger}
	 * @return				A new {@link ListLedger}
	 */
	public static <T> ListLedger<T> create(Ref<? extends ListBox<? extends T>> model, RecordTransformer<T> transformer) {
		return new ListLedger<T>(model.features().universe(), model, transformer);
	}
	
	/**
	 * Create a {@link ListLedger} with a 
	 * @param valueClass	The class of list element.
	 * @param model			The model, a {@link Ref} to the {@link ListBox} 
	 * 						whose contents we are based on
	 * @param <T>			The type of list element. 
	 * @return				A new {@link ListLedger}
	 */
	public static <T> ListLedger<T> create(Class<T> valueClass, Ref<? extends ListBox<? extends T>> model) {
		return new ListLedger<T>(model.features().universe(), model, RecordTransformerDirect.create(valueClass, "", model.features().universe()));
	}
	
	@Override
	public BoxFeatures features() {
		return features;
	}
	
	private T item(int record) {
		//We take this chance to cache the current listBox we are viewing 
		listBox = model.get();
		if (listBox == null) {
			return null;
		}
		return listBox.get(record);
	}
	
	@Override
	public int recordCount() {
		//We take this chance to cache the current listBox we are viewing 
		listBox = model.get();
		if (listBox == null) {
			return 0;
		}
		return listBox.size();
	}
	
	//Reactions used by this Ledger
	/**
	 * A {@link Reaction} that reacts to changes in our {@link RecordTransformer} by
	 * showing that we will have column changes.
	 */
	private class TransformerReaction implements Reaction {

		Map<Box, Change> columnsChanged = new IdentityHashMap<Box, Change>();
		
		private TransformerReaction() {
			columnsChanged.put(ListLedger.this, new LedgerChangeDefault(true, true, false));
		}
		
		@Override
		public void apply(boolean propagate) {
			//Nothing to do when we apply - we just propagate changes
		}
		@Override
		public boolean completeChanges(Map<Box, Change> changes) {
			//Nothing to do
			return false;
		}

		@Override
		public Map<Box, Change> react(Map<Box, Change> changes) {
			//We only listen to the RecordTransformer, so
			//if we see any change, it indicates a change of
			//just the columns of the ledger
			return columnsChanged;
		}
	}
	
	/**
	 * A {@link Reaction} that reacts to changes in our {@link ListBox} by
	 * showing that we will have list changes.
	 */
	private class ListReaction implements Reaction {

		Map<Box, Change> rowCountDifferent = new IdentityHashMap<Box, Change>();
		Map<Box, Change> rowCountSame = new IdentityHashMap<Box, Change>();
		
		private ListReaction() {
			rowCountDifferent.put(ListLedger.this, new LedgerChangeDefault(true, false, true));
			rowCountSame.put(ListLedger.this, new LedgerChangeDefault(true, false, false));
		}
		
		@Override
		public void apply(boolean propagate) {
			//Nothing to do when we apply - we just propagate changes
		}
		@Override
		public boolean completeChanges(Map<Box, Change> changes) {
			//Nothing to do
			return false;
		}

		@Override
		public Map<Box, Change> react(Map<Box, Change> changes) {
			
			//If the ref has had a new instance set then we may have row count change, and
			//cannot keep using the cached ListBox. We also can't do anything but a complete
			//list change if we have no cached listBox
			if (listBox == null || changes.get(model).shallow()) {
				listBox = null;
				return rowCountDifferent;
				
			//If the ref still points to the same list instance, see what change it has had
			} else {
				Change change = changes.get(listBox);
				//We can only reduce change if we have a ListChange
				if (change instanceof ListChange) {
					
					//Summarise to a single delta
					ListChange listChange = (ListChange) change;
					ListDelta summaryDelta = ListDeltaDefault.summarise(listChange);
					
					//If we just have alteration, row count is same
					if (summaryDelta.getType() == CollectionDeltaType.ALTERATION) {
						return rowCountSame;
						
					//Any other change indicates a different row count
					} else {
						return rowCountDifferent;
					}
					
				//If we have some other change type, assume row count change
				} else {
					listBox = null;
					return rowCountDifferent;
				}
			}
		}
	}
	
	//Methods delegated nearly directly to our RecordTransformer
	
	@Override
	public boolean editable(int record, int field) {
		return transformer.editable(record, item(record), field);
	}

	@Override
	public Class<?> fieldClass(int field) {
		return transformer.fieldClass(field);
	}

	@Override
	public int fieldCount() {
		return transformer.fieldCount();
	}

	@Override
	public String fieldName(int field) {
		return transformer.fieldName(field);
	}

	@Override
	public Object get(int record, int field) {
		return transformer.get(record, item(record), field);
	}

	@Override
	public void set(int record, int field, Object value) {
		transformer.set(record, item(record), field, value);
	}
}
